;用于执行真正的操作系统加载程序，做好操作系统运行的准备工作
%include "lib/global.inc"
SECTION LoaderData vstart=LOADER_BASE_ADDR
    ;先定义段描述符（恰占一扇区）
    ;结构为
    ;低32位:          |              段基址15~0           |              段界限15~0            |
    ;高32位:          |段基址31~24|G|D/B|L|AVL|段界限19~16| P | DPL | S | TYPE4位 | 段基址23~16 |
    ;高32位解释：      S字段指定系统段还是非系统段。0-系统段
    ;                 TYPE字段指定描述符的类型,系统段是标识各种门结构，非系统段则由X,  C/E， R/W,, A(X——可执行，R/W——代码段可读/数据段可写，C/E——一致性代码段[转移进来不改变特权级，但段特权级高]/数据扩展方向，A——是否访问过)四位组成  
    ;                 DPL段特权级
    ;                 P段是否在内存中
    ;                 AVL可用位，无作用
    ;                 L 64位代码段
    ;                 D/B 代码段标识地址位数，栈段标识操作数大小。1为32位
    ;                 G段界限粒度单位，1为4k,0为1b
    GDT_BASE:           dd 	0000_0000h
                        dd 	0000_0000h
    CODE_DESC:          dd 	0000_FFFFh
                        dd	00h      	<<DESC_BASE_31TO24_OFFSET | \
                        	DESC_G_4K | DESC_D_32 | !DESC_L_64 | !DESC_AVL_1 | \
                        	0Fh     	<<DESC_LIMIT_19TO16_OFFSET | \
                        	DESC_P_1 | DESC_DPL_0 | DESC_S_USR | \
                        	DESC_TYPE_X_EXE | !DESC_TYPE_C_COR | !DESC_TYPE_R | !DESC_TYPE_A | \
                        	00h     	<<DESC_BASE_23TO16_OFFSET
    ;非系统段，可执行，非依从，不可读，未访问。0特权级。在内存中。地址32位，段粒度4k,
    ;段基址0x0,段界限0xfffff*4k           
    DATA_DESC:          dd 	0000_FFFFh
						dd  00h      	<<DESC_BASE_31TO24_OFFSET | \
                            DESC_G_4K | DESC_B_32 | !DESC_L_64 | !DESC_AVL_1 | \
                            0Fh     	<<DESC_LIMIT_19TO16_OFFSET | \
                            DESC_P_1 | DESC_DPL_0 | DESC_S_USR | \
                            !DESC_TYPE_X_EXE | !DESC_TYPE_E_DOWN | DESC_TYPE_W | !DESC_TYPE_A | \
                            00h     	<<DESC_BASE_23TO16_OFFSET
    ;非系统段，不可执行，向上扩展，可写，未访问，0特权级。在内存中，地址32位，段粒度4k
    ;段基址0x0,段界限0xfffff*4k
    VIDEO_DESC:         dd 	8000_0007h
						dd  00h      	<<DESC_BASE_31TO24_OFFSET | \
                            DESC_G_4K | DESC_B_32 | !DESC_L_64 | !DESC_AVL_1 | \
                            0h     		<<DESC_LIMIT_19TO16_OFFSET | \
                            DESC_P_1 | DESC_DPL_0 | DESC_S_USR | \
                            !DESC_TYPE_X_EXE | !DESC_TYPE_E_DOWN | DESC_TYPE_W | !DESC_TYPE_A | \
                            0Bh     	<<DESC_BASE_23TO16_OFFSET
    ;非系统段，不可执行，向上扩展，可写，未访问,0特权级。在内存中，地址32位，段粒度4k
    ;段基址0xb800, 段界限0x7*4k
    times 60 dq 0
    GDT_LIMIT equ $ - GDT_BASE - 1
    ;整个GDT的段界限
    ;预留60个描述符的位置
    times LOADER_GDT_LIMIT - ($ - $$) db 0
    
;-------------------------------------------------------------------------------
    ;地址0x700：Loader数据段的第二个扇区
    global memoryBytes
    memoryBytes     dd 0

    ARDS_Struct     times 5 dd 0
    
    ;GDT指针，前2字节gdt界限，后4字节gdt基址，共48位
    gdt_ptr dw GDT_LIMIT
            dd GDT_BASE

    caa db "Hello World!"
    caaLen dw $ - caa

    times LOADER_CODE_ADDR - LOADER_BASE_ADDR - ($ - $$) db 0

SECTION loader vstart=LOADER_CODE_ADDR
; 调用int 0x15/0xe820中断, 检测当前系统的内存总量并且存储到0x700中
;-------------------------------------------------------------------------------
CheckMemory:
    xor ebx, ebx		      ;第一次调用前，将ebx置为0, 后续不用再管
.GetARDS:	                   ;循环获取每个ARDS内存范围描述结构
    mov eax, 0xe820
    mov di, ARDS_Struct	      ;ards结构缓冲区
    mov edx, 0x534d4150	      ;edx始终为这个值
    mov ecx, 20		          ;ARDS结构大小，始终是20字节
    int 0x15
    mov ecx, ARDS_Struct      ;回归ARDS的内存地址
    mov eax, [ecx]	          ;基址低32位
    add eax, [ecx + 8]	      ;界限低32位
    mov edx, [memoryBytes]
    cmp eax, edx
    jb .less_than_old
    mov [memoryBytes], eax
.less_than_old:
    cmp ebx, 0		      ;若ebx为0且cf不为1,这说明ards全部返回，当前已是最后一个
    jne .GetARDS
;-------------------------------------------------------------------------------

    ;随便打印点东西
    mov dx, caa
    mov ah, 09h

    mov ah, 13h
    mov al, 1
    mov bh, 0
    mov bl, 0xA4
    mov cx, [caaLen]
    mov dx, 0
    mov bp, caa
    int 10h

    ;打开A20地址线开关
    ;system control port A第1位
    in al, 0x92
    or al, 1<<1
    out 0x92, al

    ;加载全局段描述符表
    lgdt [gdt_ptr]

    ;打开保护模式
    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp dword SELECTOR_CODE:protectStart

[bits 32]
protectStart:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov byte [gs:160], 'P'

;-------------------------------------------------------------------------------
    ;打开分页

    ;第一步：初始化页表和页目录
    call SetPage

    ;gdt_ptr（共6个字节）的后四个字节是段基址
    mov ebx, GDT_BASE

    ;将GDT升高到3G以后去
    or dword [gdt_ptr + 2], 0xc0 << DESC_BASE_31TO24_OFFSET

    ;第二步：设置cr3寄存器为GDT基址
    mov eax, PAGE_DIR_BASE
    mov cr3, eax

    ;第三步：打开cr0的pg位(第 31 位)
    mov eax, cr0
    or eax, 1 << 31
    mov cr0, eax

    ;重新加载 GDT
    lgdt [gdt_ptr]

    ;测试分页是否起到效果
    mov byte [gs:360], 'V'
    ; mov eax, gs
    ; or eax, 0xc0 << DESC_BASE_31TO24_OFFSET
    ; mov gs, eax
    ; mov byte [gs:362], 'W' 

    ;读取kernel程序
    call FindKernel
    mov esi, eax 
    mov edi, KERNEL_TEMP_ADDR
    call ReadKernel

;-------------------------------------------------------------------------------
LoadKernel:
    xor eax, eax
    xor ebx, ebx
    ;程序头表地址， e_phoff
    xor ecx, ecx
    ;程序头数量, e_phnum
    xor edx, edx
    ;程序头大小, e_phentsize

    mov dx, [KERNEL_TEMP_ADDR + e_phentsize_offset]
    ;e_phentsize, ph的条目大小
    mov ebx, [KERNEL_TEMP_ADDR + e_phoff_offset]
    ;第一个ph在文件中的偏移量
    add ebx, KERNEL_TEMP_ADDR
    ;当前基底为第一个ph头
    mov cx, [KERNEL_TEMP_ADDR + e_phnum_offset]
    ;ph的数量

.read_ph:
    mov al, byte [ebx + p_type_offset]
    cmp al, PT_NULL
    je .PTEQUNULL

    ;复制程序段，esi为src, edi为dst, ecx为size
    cld ;正向复制
    push ecx
    mov ecx, [ebx + p_filesz_offset]
    ;段大小
    mov eax, [ebx + p_offset_offset]
    add eax, KERNEL_TEMP_ADDR
    mov esi, eax
    ;目标位置
    mov edi, [ebx + p_vaddr_offset]  
    rep movsb 
    pop ecx

.PTEQUNULL:    
    add ebx, edx
    ;更新基础地址为下一个程序段
    loop .read_ph

    ;最后转向目标程序
    mov esp, KERNEL_STACK_TOP
    mov ebp, KERNEL_STACK_TOP
    jmp dword [KERNEL_TEMP_ADDR + KERNEL_ENTRY_OFFSET]
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------

;-------------------------------------------------------------------------------
;name : SetPage
;function : 初始化页表和页目录
;input :
;       void
;output :
;       void
;-------------------------------------------------------------------------------
;在这里初始化页表和页目录
;页目录的结构：
;			| 页表物理页地址31~12 | AVL:3位 | G |  0  | D | A | PCD | PWT | US | RW | P |
;页表的结构：
;			|   物理页地址31~12	  | AVL:3位 | G | PAT | D | A | PCD | PWT | US | RW | P |
;含义解释:  AVL可用位
;			G全局位，置为1则快表中将始终保存该页对应的地址
;			PAT 页属性表位，再页面一级的粒度设置内存属性
;			D Dirty脏页位，CPU对页面执行写入以后会置为1，只对页表有效
;			A Accessed访问位，是否被CPU访问过
;			PCD 页级告诉缓存静止位，为1表示对该页启用高速缓存			
;			PWT 页级通写位，若为1表示采用通写方式，这是高速缓存的一种工作方式
;			US 普通用户/超级用户，前者所有程序可以访问，后者只有0,1,2特权级可以访问
;			RW 为1可读可写，为0可读不可写
;			P 存在于物理内存中
SetPage:
    mov eax, 0x1000
	mov esi, 0

;清空页目录所占的内存
.clearPage:
	mov byte [PAGE_DIR_BASE + esi], 0
	inc esi
	loop .clearPage

.createPageDir:
    ;指向低端4M内存的页目录项
	mov eax, PAGE_DIR_BASE
	add eax, PAGE_SIZE
    mov ebx, eax    ;暂时存储，等下用来建立PTE
	or eax, PAGE_US_U | PAGE_RW_W | PAGE_P
    ;物理地址0x101000, 用户特权级，可写，在内存中。

    mov [PAGE_DIR_BASE + 0x0], eax
    mov [PAGE_DIR_BASE + 0xc00], eax
    ;将第一个页表放到第一个目录和第0xc00个目录处

    sub eax, PAGE_SIZE
    mov [PAGE_DIR_BASE + 0x1000 - 4], eax
    ;将最后一个目录表指向目录表自己的地址

    ;对低端1M内存建立页表映射
    ;共占用0x100000 / 0x1000 = 0x100个页表项
    mov ecx, 0x100
    ;第一个页表的地址上面存储在ebx里
    mov esi, 0

    xor edx, edx
    or edx, PAGE_US_U | PAGE_RW_W | PAGE_P
    ;映射地址    0 ~ 0x100000 
    ;        ->  0 ~ 0x100000
    ;用户特权级，可写，存在内存中 
.create_L1M_PTE:
    ;生成低端1M的页表项
    mov [ebx + esi * 4], edx
    add edx, 0x1000
    inc esi
    loop .create_L1M_PTE

;-------------------------------------------------------------------------------
    ;生成内核部分的页表项
    ;空间映射: 3G~4G -> 0~
    ;生成内核空间的页表项，从页目录的0xc00 + 4,开始到0x1000 - 4, 共0x400 / 4 - 2 = 0x100 - 2
    ;一共有0x400个页目录，最高0x100是内核，用了一个页表和一个低端1M字节。
    ; mov eax, PAGE_DIR_BASE
    ; add eax, 0x2 << PAGE_INDEX_OFFSET
    ; or eax, PAGE_US_U | PAGE_RW_W | PAGE_P
    ; ;eax——第二个页目录项
    ; mov ebx, PAGE_DIR_BASE
    ; mov ecx, 0x100 - 2
    ;还剩254个页表项
    ; mov esi, (0xc00 >> 2) + 1
    ;从第769个页目录项开始
; .create_kernel_pde:
;     mov [ebx + esi * 4], eax
;     inc esi
;     add eax, 1 << PAGE_INDEX_OFFSET
;     loop .create_kernel_pde
    ret
;特殊操作：
;   获得页目录表物理地址：0xfffff000
;   获得页表物理地址:     0xfffffxxx    xxx为页表索引*4
;   获得页表项物理地址:   0xffc00000 + 页目录项下标<<12 + 页表项索引


;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------

;name : readDisk
;function : 读取磁盘数据。仅仅支持256个扇区以下的读写。
;input : 
;       eax —— 起始LBA扇区号
;       es:(e)di —— 数据写入的内存地址(实模式下di,保护模式下edi)
;       ecx —— 读入扇区数(ecx < 256)
;output :
;       void
readDisk:	   
    ;因为汇编中位移多位只能用cl，因此必须先保存这个寄存器
    mov ebx, ecx		   ; 备份扇区数

;第1步：将LBA地址存入0x1f3 ~ 0x1f6
    ;LBA地址7~0位写入端口0x1f3
    mov dx, 0x1f3                       
    out dx, al                          

    ;LBA地址15~8位写入端口0x1f4
    mov cl, 8
    shr eax, cl
    mov dx, 0x1f4
    out dx, al

    ;LBA地址23~16位写入端口0x1f5
    shr eax, cl
    mov dx, 0x1f5
    out dx, al

    ;LBA地址24~27和device的模式和盘型输入device端口0x1f6
    shr eax, cl
    and al, 0000_1111b	   ;lba第24~27位
    or  al, 1110_0000b	   ; 设置7～4位为1110,表示lba模式,主盘
    mov dx, 0x1f6
    out dx, al

;第2步：设置要读取的扇区数
    mov dx, 0x1f2
    mov al, bl
    out dx, al 

;第3步：向0x1f7端口写入读命令，0x20 
    mov dx, 0x1f7
    mov al, 0x20                        
    out dx, al

;硬盘控制器0x1f7位立刻变为状态寄存器
;第4步：检测硬盘状态
.checkDisk:		   ;测试0x1f7端口(status寄存器)的的BSY位
    in al, dx
    and al, 1000_1000b	   ;第4位为1表示硬盘控制器已准备好数据传输,第7位为1表示硬盘忙
    cmp al, 0000_1000b
    jnz .checkDisk	   ;若未准备好,继续等。

;第5步：从0x1f0端口读数据
    mov ax, bx     ;ebx存着迭代次数
    mov edx, 256	   ;一个扇区有512字节,每次读入一个字,共需cx*512/2次,所以cx*256
    mul dx	   
    mov cx, ax     ;256*256能够用一个16位寄存器存储下来
    mov dx, 0x1f0
    rep insw       ;es:edi在参数中已经设置好
    ret

;-------------------------------------------------------------------------------
;name : FindKernel
;function : 在目录表中找到Kernel.bin文件
;input :
;       void
;output :
;       eax -- Loader.bin目录项起始簇号

;-------------------------------------------------------------------------------
FindKernel:
    mov edx, FDT_SEC_ID
;最外层循环，遍历FDT的扇区
.FdtSecLoop:
    mov ecx, 1
    mov eax, edx   
    mov edi, FDT_TEMP_ADDR
    call readDisk
    sub edi, FAT_BPS
;中层循环，遍历一个扇区里的FDT表项
.FdtItemLoop:
    xor esi, esi
;内层循环，比较当前表项文件名是否为loader.bin
.FdtNameCmp:
    mov al, byte [KernelName + esi]
    cmp al, byte [edi + esi]
    jne .NotMatched
    inc esi
    cmp esi, KERNEL_NAME_SIZE
    jne .FdtNameCmp
;若匹配成功，直接返回
    xor eax, eax
    mov ax, word [edi + FDI_START_OFFSET]
    ret
;若匹配失败，进入下一个表项
.NotMatched:      
    add edi, FDT_ITEM_SIZE
    mov ecx, FAT_BPS + FDT_TEMP_ADDR
    cmp edi, ecx
    jne .FdtItemLoop
    inc edx
    cmp edx, FDT_SEC_ID + FDT_SEC_SIZE
    jne .FdtSecLoop
    ;失败则进入死循环
    hlt
KERNEL_NAME_SIZE    equ 11
KernelName      db "KERNEL  BIN"

;-------------------------------------------------------------------------------
;name : ReadKernel
;function : 根据起始簇号读入Kernel
;input :
;       esi -- Kernel.bin的起始簇号
;       es:edi -- 要读入Kernel的内存位置
;output :
;       void
;-------------------------------------------------------------------------------
ReadKernel:
    push edi
    ;读入FAT目录表
    mov edi, FAT_TEMP_ADDR
    mov eax, 1
    mov ecx, 1
    call readDisk
    pop edi
;依次遍历Kernel文件的每一个簇，将其读入内存中
NotEndCluster:
    lea eax, [FAT_SEC_OFFSET + esi * FAT_RsvdSecCnt]
    mov ecx, FAT_RsvdSecCnt
    call readDisk
    mov si, word [FAT_TEMP_ADDR + esi * 2]
    cmp si, 0xFFFF
    je  EndCluster
    jmp NotEndCluster
EndCluster:
    ret